在圣诞活动中遇到一个需求,要实现将物体可以装饰在圣诞树中

大概需求如上,从下方拖拽道具,可以放到树上,下面的道具以及树都是图片,如果拖拽区域不在树上,自然不能拖放,最终的实现效果如下:(忽略这个toast,可以去掉)
视频中可以看到几个功能点:
- 点击下方道具库可以添加到树上
- 已经在树上的道具可以随意拖拽
- 拖拽超出三角形的范围之后为无效,会自动回到上一次拖拽位置
这个需求中涉及的难点主要是,如何才能对两张图做一个准确的碰撞检测?刚开始实现的想法,是做一个三角形检测函数,即给定三个点的坐标,然后判断当前装扮物的位置是否在三个点构成的三角形中,但是发现存在一些问题:圣诞树的边缘并不规则,如果仅仅是一个三角形计算,会导致周围真空区域也可以拖拽到

可以看到,在这种情况之下,不规则的边缘并不能很好的实现检测,所以这里想到了之前看过的一个技术文章
https://juejin.cn/post/6844903846221725710《使用 getImageData 实现碰撞检测》
这个文章中大概也是介绍了一下,基于canvas的getImageData来实现碰撞检测
主要实现思路
参考上面的这篇文章,圣诞树主要思路是,在canvas画布拖拽的时候,实时去检测到当前拖住点的rgba的值,判断透明度是不是存在,如果存在那么必定透明度为1,否则为0。
既然是基于canvas的getImageData来实现的,那么就需要先用canvas渲染一个圣诞树图片进去,然后,在手指触碰canvas节点时候,调用getImageData,传入x,y坐标,获取到rgba的值就可以实现。
CanvasRenderingContext2D:getImageData() 方法
看一下MDN官网对于getImageData方法的描述
Canvas 2D API 的 CanvasRenderingContext2D.getImageData() 返回一个 ImageData 对象,用于描述 canvas 指定区域的隐含像素数据。
这个方法不受画布的变换矩阵影响。如果指定的矩形超出画布的边界,返回的 ImageData 对象中超出画布边界的像素将是透明黑色
使用方法
1 2
| getImageData(sx, sy, sw, sh) getImageData(sx, sy, sw, sh, settings)
|

大概意思就是,传入对应的x,y坐标,就会返回一个imageData对象。
ImageData 对象的主要属性包括:
- width:表示图像数据的宽度(以像素为单位)。
- height:表示图像数据的高度(以像素为单位)。
- data:一个 Uint8ClampedArray 类型的数组,包含了图像数据的像素信息。每个像素由四个值组成,分别表示红色(R)、绿色(G)、蓝色(B)和透明度(A),每个值的范围是 0 到 255。
实现代码
首先在dom中绘制一个canvas元素,然后将圣诞树图片渲染好放进去

当点击道具库中的装扮时候,我这里做法粗暴一点,直接获取到这个canvas dom元素的位于屏幕的坐标,计算出来中心点,直接将道具设置为中心就可以了。
拖拽部分实现,首先给元素绑定对应的拖拽事件,touchstart,touchmove以及touchend
直接贴核心代码
handleTouchstart:这是记录了起始拖拽位置,常规代码,额外记录了cacheX和cacheY,缓存了当前位置,如果拖拽超出圣诞树,就用这个位置设置还原。
handleTouchmove:拖拽装扮的过程中,这里需要做一个坐标的计算,因为canvas 这个dom距离顶部是有距离的,我们在页面中移动手指坐标时候,需要将顶部的距离和左侧的距离计算进去(这里调用像素检测函数,是在测试时方便判断,判断是否在圣诞树当中的)
handleTouchEnd:调用像素检测函数getPixel,判断是否在圣诞树上,不在的话还原上一次的值,否则更新
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79
| // 拖拽道具逻辑 let diffX = 0; let diffY = 0; let clientX = 0; let clientY = 0; const handleTouchstart = (event: TouchEvent, item) => { diffX = event.touches[0].clientX - (event.target!.offsetLeft - event.target!.clientWidth / 2); diffY = event.touches[0].clientY - (event.target!.offsetTop - event.target!.clientHeight / 2);
clientX = event.touches[0].clientX - event.target!.clientWidth / 2; clientY = event.touches[0].clientY - event.target!.clientHeight / 2;
// 记录下来原始的坐标信息,如果touchend时候超出范围,重新设置为起始位置 // item.cacheX = clientX - diffX; // item.cacheY = clientY - diffY; item.cacheX = event.target!.style.left; item.cacheY = event.target!.style.top; };
const handleTouchmove = (event: TouchEvent, item) => { event.preventDefault(); const canvasTop = treeCanvasRef.value!.getBoundingClientRect().top; const canvasLeft = treeCanvasRef.value!.getBoundingClientRect().left; const left = event.touches[0].clientX - diffX; const top = event.touches[0].clientY - diffY; item.pos_x = left; item.pos_y = top; clientX = event.touches[0].clientX - event.target!.clientWidth / 2; clientY = event.touches[0].clientY - event.target!.clientHeight / 2; // 测试使用 getPixel(clientX - canvasLeft, clientY - canvasTop).then(res => { console.log(res, item, "拖拽范围判断"); }); };
const handleTouchEnd = item => { const canvasTop = treeCanvasRef.value!.getBoundingClientRect().top; const canvasLeft = treeCanvasRef.value!.getBoundingClientRect().left; getPixel(clientX - canvasLeft, clientY - canvasTop).then(async res => { if (!res) { // 不可以拖拽的区域,重新设置为起始位置 console.log("不可以拖拽的区域,重新设置为起始位置"); item.pos_x = item.cacheX; item.pos_y = item.cacheY; return; } // const toast = Toast.loading({ // forbidClick: true // }); const params: IChristmasDecorateTreeRequest = { props_pos: { pos_id: item.pos_id, props_type: item.props_type, pos_x: item.pos_x, pos_y: item.pos_y // pos_x: clientX - canvasLeft, // pos_y: clientY - canvasTop }, tree_id: String(storeTreeData.userTreeConfig.tree_id), user_id: userStore.userInfo.userId }; try { const res = await postChristmasEditTree(params); if (res.code !== 0) { Toast(res.msg); } console.log(params, "拖拽结束"); } catch (error) { Toast(String(error)); } // finally { // toast.clear(); // } }); };
|
像素检测的核心代码,判断data[3]/255的值是否为1,是的话就是有像素,在圣诞树上,否则的话就是在透明区域,不在圣诞树当中
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| const getPixel = (x: number, y: number) => { return new Promise(resolve => { // 减去距离顶部的距离 const pixel = ctx.getImageData(x, y, 1, 1); const data = pixel.data; const rgba = `rgba(${data[0]}, ${data[1]}, ${data[2]}, ${data[3] / 255})`; rgb.value = rgba;
if (data[3] / 255 === 1) { resolve(true); } else { resolve(false); } }); };
|
上面的代码路径在 apps/h5-client/livevoice-business/src/views/2024christmas/hooks/tree/use-tree.ts 中
本文标题:基于canvas getImageData函数的图片碰撞检测
文章作者:Niuhk
发布时间:2025-01-21
最后更新:2025-02-12
原始链接:https://www.niuhk.cn/2025/01/21/基于canvas getImageData函数的图片碰撞检测/
版权声明:转载请注明出处!